Google编程规范:
Google的C++开源项目编程规范
C++编程规范
1,头文件
1.1 头文件可以自给自足,可以作为第一个引入的头文件
1.2 define保护
项目 foo 中的头文件 foo/src/bar/hd.h 可按如下方式保护:
1 |
|
1.3 尽量避免使用前置声明。使用 #include 包含需要的头文件即可。
前置声明就是我在声明一个类(CHouse)的时候,用到了另外一个类的定义(CBed),但是CBed还没有定义呢,而且我还先不需要CBed的定义,只要知道CBed是一个类就够了。那好,我就先声明类CBed,告诉编译器CBed是一个类(不用包含CBed的头文件,然后在CHouse中用到CBed的,都用CBed的指针类型代(因为指针类型固定大小的,但是CBed的大小只用知道了CBed定义才能确定)。等到要实现CHouse定义的时候,就必须要知道CBed的定义了,那是再包好CBed的头文件就行了。
- 尽量避免前置声明那些定义在其他项目中的实体.
- 函数:总是使用
#include. - 类模板:优先使用
#include.
1.4 只有当函数少于10行时才将其定义为内联函数.
当函数被声明为内联函数之后, 编译器会将其内联展开, 而不是按通常的函数调用机制进行调用.
不内联的情况:
- 循环或switch语句
- 析构函数,虚函数,递归函数
内联函数必须放在 .h 文件中. 如果内联函数比较短, 就直接放在 .h 中.
1.5 #include的路径和顺序
dir/foo.cc 或 dir/foo_test.cc 的主要作用是实现或测试 dir2/foo2.h 的功能, foo.cc 中包含头文件的次序如下:
- cpp直接的头文件(dir2/foo2.h)
- C库
- C++库
- 其他库的头文件
- 项目内的头文件
这种优先的顺序排序保证当 dir2/foo2.h 遗漏某些必要的库时, dir/foo.cc 或 dir/foo_test.cc 的构建会立刻中止。因此这一条规则保证维护这些文件的人们首先看到构建中止的消息而不是维护其他包的人们。
按字母顺序分别对每种类型的头文件进行二次排序是不错的主意
您所依赖的符号 (symbols) 被哪些头文件所定义,您就应该包含(include)哪些头文件,哪怕您所包含的 foo.h 已经包含了 bar.h, 也照样得包含 bar.h, 除非 foo.h 有明确说明它会自动向您提供 bar.h 中的 symbol.
2,作用域
2.1 命名空间
禁止使用using
1 | // .h 文件 |
1 | // .cc 文件 |
2.2 匿名命名空间
1 | namespace { |
推荐、鼓励在 .cc 中对于不需要在其他地方引用的标识符使用内部链接性声明 (匿名命名空间和static),但是不要在 .h 中使用。
所有置于匿名命名空间的声明都具有内部链接性,名称的作用域被限制在当前文件中,无法通过在另外的文件中使用extern声明来进行链接
函数和变量可以经由声明为 static 拥有内部链接性,这意味着你在这个文件中声明的这些标识符都不能在另一个文件中被访问。即使两个文件声明了完全一样名字的标识符,它们所指向的实体实际上是完全不同的。
2.3 静态成员函数与命名空间非成员函数
尽量不要使用全局函数,非成员函数不应依赖于外部变量, 应尽量置于某个命名空间内,不要使用静态成员方法,类的静态方法应当和类的实例或静态数据紧密相关.
1 | namespace myproject { |
1 | namespace myproject { |
2.4 将函数变量尽可能置于最小作用域内, 并在变量声明时进行初始化,离第一次使用越近越好
1 | //Not Like This |
if, while 和 for 语句的变量应当在这些语句中正常地声明,这样子这些变量的作用域就被限制在这些语句中了
1 | while (const char* p = strchr(str, '/')) str = p + 1; |
有一个例外, 如果变量是一个对象, 每次进入作用域都要调用其构造函数, 每次退出作用域都要调用其析构函数. 这会导致效率降低,这时应该在循环作用域外面声明这类变量:
1 | // 低效的实现 |
2.5 静态和全局变量
尽量不用全局函数和全局变量,
多线程中的全局变量 (含静态成员变量) 不要使用 class 类型 (含 STL 容器), 因为构造和析构顺序的不确定导致不明确行为导致的 bug.
静态生存周期的对象,即包括了全局变量,静态变量,静态类成员变量和函数静态变量,都必须是原生数据类型 (POD : Plain Old Data): 即 int, char 和 float, 以及 POD 类型的指针、数组和结构体。即完全禁用类类型的全局变量,比如vector(使用 C 数组替代) 和string(使用 const char [])。我们也不允许用函数返回值来初始化 POD 变量,除非该函数不涉及任何全局变量。
3,类
3.1 不要在构造函数中调用虚函数
如果在构造函数内调用了自身的虚函数, 这类调用是不会重定向到子类的虚函数实现. 即使当前没有子类化实现, 将来仍是隐患.
3.2 对于单参数构造函数, 请使用explicit关键字来禁止隐式转换
3.3 移动构造函数,移动赋值函数和拷贝构造函数,拷贝赋值函数
1 | //拷贝构造函数 |
3.3.1 拷贝构造函数:
1 | MyClass foo; |
什么时候用到拷贝构造函数?
- 一个对象以值传递的方式传入函数体;
- 一个对象以值传递的方式从函数返回;
- 一个对象需要通过另外一个对象进行初始化
3.3.2 深拷贝
如果一个类拥有资源,当这个类的对象发生复制过程的时候,资源重新分配,例如需要新开辟内存,这个过程就是深拷贝,反之,没有重新分配资源,就是浅拷贝。
如对象A=B。这时,如果B中有一个成员变量指针已经申请了内存,浅拷贝(默认拷贝构造函数)则是A中的那个成员变量也指向同一块内存。这就出现了问题:当B把内存释放了(如:析构),这时A内的指针就是野指针了,出现运行错误。
3.3.3 移动构造函数
移动实现的是对象值真实的转移(源对象到目的对象):源对象将丢失其内容,其内容将被目的对象占有。移动操作的发生的时候,是当移动值的对象是未命名的对象的时候。 这里未命名的对象就是那些临时变量,甚至都不会有名称。
典型的未命名对象就是函数的返回值或者类型转换的对象。
使用临时对象的值初始化另一个对象值,不会要求对对象的复制:因为临时对象不会有其它使用,因而,它的值可以被移动到目的对象。做到这些,就要使用移动构造函数和移动赋值:
1 | MyClass fn(); // 函数返回一个 MyClass 对象 |
3.3.4 区别:
拷贝和移动是不同的操作:从A拷贝到B意味着,B分配了新内存,A的整个内容被拷贝到为B分配的新内存上。 而从A移动到B意味着分配给A的内存转移给了B,没有分配新的内存,它仅仅包含简单地拷贝指针。
3.3.5 总结
如果需要就让你的类型可拷贝 / 可移动. 作为一个经验法则, 如果对于你的用户来说这个拷贝操作不是一眼就能看出来的, 那就不要把类型设置为可拷贝. 如果让类型可拷贝, 一定要同时给出拷贝构造函数和赋值操作的定义, 反之亦然. 如果让类型可拷贝, 同时移动操作的效率高于拷贝操作, 那么就把移动的两个操作 (移动构造函数和赋值操作) 也给出定义. 如果类型不可拷贝, 但是移动操作的正确性对用户显然可见, 那么把这个类型设置为只可移动并定义移动的两个操作.
如果你的类不需要拷贝 / 移动操作, 请显式地通过在 public 域中使用 = delete 或其他手段禁用之.
1 | // MyClass is neither copyable nor movable. |
3.4 仅当只有数据成员时使用struct,其它一概使用class
3.5 继承
组合 > 实现继承 > 接口继承 > 私有继承
3.5.1 组合
组合类:一个类(Head类)里面的数据成员是另一个类的对象,即内嵌其他类的对象作为自己的成员。
Eye,Nose,Mouth,Ear这些类都是Head类的一部分
1 | class Eye{void look()}; |
如果A类是B类的一部分,则不要让B类去继承A类,而是采用组合的形式。如果A类和B类毫无关系,我们不应该为了让B类多一个功能而去让B继承A。如果在逻辑上B是A的一种,即B类的man也是A类的 Hunman的一种我们就可以让B类去继承A类。
3.5.2 继承
所有继承必须是 public 的. 如果你想使用私有继承, 你应该替换成把基类的实例作为成员对象的方式.
对于可能被子类访问的成员函数, 不要过度使用 protected 关键字.
数据成员都必须是 私有的.
##### 3.5.3 虚函数
能够根据指针所指类型调用对应的类对象,实现函数调用时的多态性。
虚函数必须声明是指针才有多态,即基类的指针指向了子类的对象,虚函数调用的是子类对象的函数,不是虚函数则调用的是基类对象的函数。
基类的析构函数应该是虚函数。
派生类对象构造的时候先调用基类的构造函数再调用派生类的构造函数,析构的时候先调用派生类析构函数再调用基类析构函数。派生类的析构函数会只析构自己的那部分,这时候如果基类的析构函数不是虚函数,则不能调用基类的析构函数析构从基类继承来的那部分成员,所以就会出现只删一半的现象,造成内存泄漏。
1 | //不管析构函数是否是虚函数(即是否加virtual关键词),delete时基类和子类都会被释放; |
3.6 声明顺序
类定义一般应以 public: 开始, 后跟 protected:, 最后是 private:. 省略空部分.
在各个部分中, 建议将类似的声明放在一起, 并且建议以如下的顺序:
- 类型 (包括
typedef,using和嵌套的结构体与类), - 常量
- 工厂函数
- 构造函数
- 赋值运算符
- 析构函数
- 其他函数
- 数据成员
不要将大段的函数定义内联在类定义中. 通常,只有那些普通的, 或性能关键且短小的函数可以内联在类定义中
4,函数
4.1 参数顺序
函数的参数顺序为: 输入参数在先, 后跟输出参数.
4.2 函数长度
如果函数超过 40 行, 可以思索一下能不能在不影响程序结构的前提下对其进行分割.
4.3 引用参数
所有按引用传递的参数必须加上 const。
输入参数是值参或 const 引用,输入参数可以是 const 指针,但决不能是非 const 的引用参数
输出参数为指针.
1 | void Foo(const string &in, string *out); |
4.4 函数重载
如果打算重载一个函数, 可以试试改在函数名里加上参数信息. 例如, 用 AppendString() 和 AppendInt() 等, 而不是一口气重载多个 Append(). 如果重载函数的目的是为了支持不同数量的同一类型参数, 则优先考虑使用 std::vector 以便使用者可以用 列表初始化 指定参数.
5,其他
5.1 使用 C++ 的类型转换, 如 static_cast<>(). 不要使用int y = (int)x等转换方式
5.2 禁止使用 RTTI.
5.3 不使用 C++ 异常.
5.4 只在记录日志时使用流.
不要使用流, 除非是日志接口需要. 使用 printf 之类的代替.
5.5 对于迭代器和其他模板对象使用前缀形式 (++i) 的自增, 自减运算符.
对简单数值 (非对象), 两种都无所谓. 对迭代器和模板类型, 使用前置自增 (自减),不考虑返回值的话, 前置自增 (++i) 通常要比后置自增 (i++) 效率更高. 因为后置自增 (或自减) 需要对表达式的值 i 进行一次拷贝. 如果 i 是迭代器或其他非数值类型, 拷贝的代价是比较大的.
5.6 const
const变量, 数据成员, 函数和参数为编译时类型检测增加了一层保障; 便于尽早发现错误.
注意初始化 const 对象时,必须在初始化的同时值初始化。
我们强烈建议在任何可能的情况下使用 const
- 如果函数不会修改传你入的引用或指针类型参数, 该参数应声明为
const. - 尽可能将函数声明为
const. 访问函数应该总是const. 其他不会修改任何数据成员, 未调用非const函数, 不会返回数据成员非const指针或引用的函数也应该声明成const. - 如果数据成员在对象构造之后不再发生变化, 可将其定义为
const.
5.7 constexpr
变量可以被声明成 constexpr 以表示它是真正意义上的常量,即在编译时和运行时都不变。函数或构造函数也可以被声明成 constexpr, 以用来定义 constexpr 变量
5.8 预处理宏
用宏展开性能关键的代码, 现在可以用内联函数替代.
用宏表示常量可被const变量代替.
用宏 “缩写” 长变量名可被引用代替.
使用宏时,应遵守:
- 不要在
.h文件中定义宏. - 在马上要使用时才进行
#define, 使用后要立即#undef. - 不要只是对已经存在的宏使用#undef,选择一个不会冲突的名称;
- 不要试图使用展开后会导致 C++ 构造不稳定的宏, 不然也至少要附上文档说明其行为.
- 不要用
##处理函数,类和变量的名字。
5.9 零值
整数用 0, 实数用 0.0, 指针用 nullptr 或 NULL, 字符 (串) 用 '\0'.
5.10 尽可能用 sizeof(varname) 代替 sizeof(type).
6,命名规范
6.1 文件命名
文件名要全部小写, 可以包含下划线 (_)
6.2 类型命名
类型名称的每个单词首字母均大写, 不包含下划线: MyExcitingClass, MyExcitingEnum
6.3 变量命名
变量 (包括函数参数) 和数据成员名一律小写, 单词之间用下划线连接,
类的成员变量以下划线结尾,但结构体的就不用。
如: a_local_variable, a_struct_data_member, a_class_data_member_.
6.4 常量命名
声明为 constexpr 或 const 的变量, 或在程序运行期间其值始终保持不变的, 命名时以k开头, 大小写混合. 例如:
1 | const int kDaysInAWeek = 7; |
6.5 函数命名
一般来说, 函数名的每个单词首字母大写,没有下划线。
对于首字母缩写的单词, 更倾向于将它们视作一个单词进行首字母大写 (例如, 写作 StartRpc() 而非 StartRPC()).
6.6 命名空间
命名空间以小写字母命名. 最高级命名空间的名字取决于项目名称. 要注意避免嵌套命名空间的名字之间和常见的顶级命名空间的名字之间发生冲突.
顶级命名空间的名称应当是项目名或者是该命名空间中的代码所属的团队的名字. 命名空间中的代码, 应当存放于和命名空间的名字匹配的文件夹或其子文件夹中.
注意 不使用缩写作为名称 的规则同样适用于命名空间. 命名空间中的代码极少需要涉及命名空间的名称, 因此没有必要在命名空间中使用缩写.
7,格式
7.1 每一行代码字符数不超过 80.
7.2 尽量不使用非 ASCII 字符, 使用时必须使用 UTF-8 编码.
7.3 函数声明与调用
返回类型和函数名在同一行, 参数也尽量放在同一行, 如果放不下就对形参分行, 分行方式与 函数调用 一致.
1 | ReturnType ClassName::FunctionName(Type par_name1, Type par_name2) { |
如果同一行文本太多, 放不下所有参数:
1 | ReturnType ClassName::ReallyLongFunctionName(Type par_name1, Type par_name2, |
注意:
- 使用好的参数名.
- 只有在参数未被使用或者其用途非常明显时, 才能省略参数名.
- 如果返回类型和函数名在一行放不下, 分行.
- 如果返回类型与函数声明或定义分行了, 不要缩进.
- 左圆括号总是和函数名在同一行.
- 函数名和左圆括号间永远没有空格.
- 圆括号与参数间没有空格.
- 左大括号总在最后一个参数同一行的末尾处, 不另起新行.
- 右大括号总是单独位于函数最后一行, 或者与左大括号同一行.
- 右圆括号和左大括号间总是有一个空格.
- 所有形参应尽可能对齐.
- 缺省缩进为 2 个空格.
- 换行后的参数保持 4 个空格的缩进.
7.4 条件语句
注意所有情况下 if 和左圆括号间都有个空格. 右圆括号和左大括号之间也要有个空格:
1 | if ( condition ) { // 圆括号与空格紧邻 - 不常见 |
7.5 循环语句与switch
1 | switch (var) { |
7.6 指针与引用
句点或箭头前后不要有空格. 指针/地址操作符 (*, &) 之后不能有空格.
- 在访问成员时, 句点或箭头前后没有空格.
- 指针操作符
*或&后没有空格.
1 | x = *p; |
7.7 变量与数组初始化
用 =, () 和 {} 均可.
1 | int x = 3; |
7.8 类格式
访问控制块的声明依次序是 public:, protected:, private:, 每个都缩进 1 个空格.
1 | class MyClass : public OtherClass { |
注意:
- 所有基类名应在 80 列限制下尽量与子类名放在同一行.
- 关键词
public:,protected:,private:要缩进 1 个空格. - 除第一个关键词 (一般是
public) 外, 其他关键词前要空一行. 如果类比较小的话也可以不空. - 这些关键词后不要保留空行.
public放在最前面, 然后是protected, 最后是private.- 关于声明顺序的规则请参考 声明顺序 一节.
7.9 初始化列表
构造函数初始化列表放在同一行或按四格缩进并排多行.
1 | // 如果所有变量能放在同一行: |
7.10 命名空间
1 | namespace { |
通用
1 |
|
1 | if (b) { // if 条件语句和循环语句关键字后均有空格. |
1 | // 赋值运算符前后总是有空格. |
1 | // 尖括号(< and >) 不与空格紧邻, < 前没有空格, > 和 ( 之间也没有. |